package com.exam.aspect;

import com.exam.annotation.Cache;
import com.exam.exception.BusinessException;
import com.exam.exception.CommonErrorCode;
import com.exam.utils.RedisUtil;
import lombok.RequiredArgsConstructor;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import java.lang.reflect.Method;
import java.util.Random;
import java.util.concurrent.TimeUnit;

import static com.exam.utils.AnnotationUtils.LogAndInvokeTargetMethod;
import static com.exam.utils.AnnotationUtils.parseSpel;

@Aspect
@Component
@RequiredArgsConstructor
public class CacheAspect {

    private final RedisUtil redisUtil;

    /**
     * 在目标方法执行前后进行环绕处理，实现缓存逻辑
     *
     * @param joinPoint 切面连接点对象
     * @return 目标方法执行结果
     * @throws Throwable 如果在环绕处理过程中发生异常，则抛出该异常
     */
    @Around("@annotation(com.exam.annotation.Cache)")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        // 获取目标方法的对象
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();

        // 获取目标方法上的缓存注解
        // 目标方法上的注解对象
        Cache cache = method.getAnnotation(Cache.class);
        // 获取缓存的键前缀
        String cacheKey = cache.prefix();
        // 如果缓存键后缀不为空
        if (StringUtils.hasLength(cache.suffix())) {
            // 解析SpEL表达式获取后缀
            String suffix = parseSpel(cache.suffix(), method, joinPoint.getArgs());
            // 拼接完整的缓存键
            cacheKey += ":" + suffix;
        }

        // 获取缓存的时间单位
        TimeUnit timeUnit = cache.timeUnit();
        // 获取缓存的存活时间
        long ttl = cache.ttl();
        // 获取缓存的随机时间
        int randomTime = cache.randomTime();
        // 将随机时间转换为对应时间单位的毫秒数
        long randomTtl = timeUnit.convert(new Random().nextInt(randomTime), timeUnit);

        // 获取执行目标方法的类
        Class clazz = joinPoint.getTarget().getClass();
        // 获取日志记录器
        Logger logger = LoggerFactory.getLogger(clazz);

        // 如果需要重置缓存
        if (cache.resetCache()) {
            // 记录日志并调用目标方法
            Object result = LogAndInvokeTargetMethod(joinPoint, logger,
                    String.format("%s中的%s方法, 准备reset cache: %s", clazz.getName(), method.getName(), cacheKey)
                    , String.format("%s中的%s方法执行结束", clazz.getName(), method.getName()));
            // 如果目标方法返回结果为空，则抛出业务异常
            if (result == null) {
                throw new BusinessException(CommonErrorCode.E_800001);
            }
            // 将结果存入缓存
            redisUtil.set(cacheKey, result, ttl + randomTtl, timeUnit);
            // 返回目标方法的执行结果
            return result;
        }

        // 在调用目标方法前检查缓存
        return beforeInvokeCheckCache(joinPoint, method, cacheKey, timeUnit, ttl, randomTtl, clazz, logger);
    }

    /**
     * 在调用目标方法前检查缓存
     *
     * @param joinPoint 切面连接点对象
     * @param method 目标方法
     * @param cacheKey 缓存键
     * @param timeUnit 时间单位
     * @param ttl 缓存存活时间
     * @param randomTtl 缓存随机存活时间
     * @param clazz 执行目标方法的类
     * @param logger 日志记录器
     * @return 缓存值或目标方法执行结果
     * @throws Throwable 如果在环绕处理过程中发生异常，则抛出该异常
     */
    private Object beforeInvokeCheckCache(ProceedingJoinPoint joinPoint, Method method, String cacheKey, TimeUnit timeUnit, long ttl, long randomTtl, Class clazz, Logger logger) throws Throwable {
        // 从缓存中获取值
        Object cacheValue = redisUtil.get(cacheKey);
        // 如果缓存中没有值
        if (cacheValue == null) {
            // 记录日志并调用目标方法
            Object result = LogAndInvokeTargetMethod(joinPoint, logger,
                    // 记录日志：类名 + 方法名 + "查询了cache: " + 缓存键
                    String.format("%s中的%s方法, 查询了cache: %s", clazz.getName(), method.getName(), cacheKey)
                    , // 记录日志：类名 + 方法名 + "执行结束"
                    String.format("%s中的%s方法执行结束", clazz.getName(), method.getName()));
            // 将结果存入缓存，存活时间为ttl + randomTtl，时间单位为timeUnit
            redisUtil.set(cacheKey, result, ttl + randomTtl, timeUnit);
            // 返回目标方法的执行结果
            return result;
        }
        // 如果缓存中有值，则直接返回缓存值
        return cacheValue;
    }
}
